﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

// Notes:
//  - Don't pass in state of objects; always query for latest state once you start executing a task.
//  - Pattern is good for serializing access to data, specifically if lots of threads are accessing
//    the same data (not the best pattern if accessesing wide-spread data).

namespace Cronus.Utils
{
    /// <summary>
    /// Private classe used to queue work on the SingleThreadWorkPump.
    /// </summary>
    internal class QueueItem
    {
        public int ID { get; private set; }
        public Action Action { get; private set; }
        public bool CancelTask { get; set; }

        QueueItem(Action action)
        {
            Action = action;
            CancelTask = false;
        }

        static int _Counter = 0;
        static object _CreationLock = new object();
        internal static QueueItem Create(Action action)
        {
            var pair = new QueueItem(action);
            lock (_CreationLock)
            {
                pair.ID = _Counter++;
            }
            return pair;
        }
    }

    /// <summary>
    /// Provides a means for queueing up work onto a single thread.
    /// </summary>
    public class SingleThreadWorkPump
    {
        AutoResetEvent _TaskAdded = new AutoResetEvent(false);
        AutoResetEvent _DoneRunningEvent = new AutoResetEvent(false);
        Queue<QueueItem> _TodoList = new Queue<QueueItem>();
        object _QueueLock = new object();
        bool _KeepRunning = true;

        SingleThreadWorkPump()
        {
        }

        /// <summary>
        /// Creates an instance and starts running the pump on it's own thread.
        /// </summary>
        /// <returns></returns>
        public static SingleThreadWorkPump Create()
        {
            // Create the pump and run the pump as a task.
            var pump = new SingleThreadWorkPump();
            Task.Factory.StartNew(() => Run(pump));
            return pump;
        }

        /// <summary>
        /// Triggers this object to stop executing queued tasks. Function returns immediately, but the pump
        /// will finish executing the currently running task.
        /// </summary>
        public void StopRunning()
        {
            lock (_QueueLock)
            {
                _KeepRunning = false;
                _TaskAdded.Set();
            }
        }

        /// <summary>
        /// Triggers the pump to stop executing queued tasks and will block until the currently executing
        /// task finishes running.
        /// </summary>
        public void StopRunningAndWaitForDone()
        {
            StopRunning();
            _DoneRunningEvent.WaitOne();
        }

        /// <summary>
        /// Guts of this class. This function is meant to be run on a thread. It pulls off a task from the queue
        /// and executes it. It will continue executing tasks until the user has triggered it to stop.
        /// </summary>
        /// <param name="pump">SingleThreadWorkPump to run.</param>
        static void Run(SingleThreadWorkPump pump)
        {
            while (pump.KeepRunning())
            {
                // Get the next task. If there isn't a task wait until the next task has been added.
                QueueItem todoNow = pump.DequeueNextTask();
                if (null == todoNow)
                {
                    pump.WaitForNext(); // Also triggers if KeepRunning() has changed.
                    todoNow = pump.DequeueNextTask();
                }

                // Execute the given task as long as it hasn't been cancelled.
                if (null != todoNow)
                {
                    if (!todoNow.CancelTask)
                    {
                        todoNow.Action();
                    }
                    todoNow = null;
                }
            }

            // Trigger the "done running" event.
            pump.DoneRunning();
        }

        #region Helpers for Run()

        bool KeepRunning()
        {
            lock (_QueueLock)
            {
                return _KeepRunning;
            }
        }

        QueueItem DequeueNextTask()
        {
            lock (_QueueLock)
            {
                if (0 < _TodoList.Count)
                    return _TodoList.Dequeue();
            }
            return null;
        }

        void WaitForNext()
        {
            _TaskAdded.WaitOne();
        }

        void DoneRunning()
        {
            _DoneRunningEvent.Set();
        }

        #endregion

        /// <summary>
        /// Adds the given task to the queue to execute later.
        /// </summary>
        /// <param name="action">Action to perform later.</param>
        /// <returns>Cookie that may be used to cancel any tasks later.</returns>
        public object AddTask(Action action)
        {
            // Create the item for the queue and the queue it up.
            var todo = QueueItem.Create(action);
            lock (_QueueLock)
            {
                _TodoList.Enqueue(todo);
                _TaskAdded.Set();
            }
            return todo.ID;
        }

        /// <summary>
        /// Allows the user to delay calling a task for a number of milliseconds. Cannot cancel a
        /// task to run later (yet).
        /// </summary>
        /// <param name="ms">Milliseconds to delay the action.</param>
        /// <param name="action">Action to perform.</param>
        /// <returns></returns>
        public object AddTaskLater(long ms, Action action)
        {
            var todo = QueueItem.Create(action);

            var timer = new Timer((state) => {
                    lock (_QueueLock)
                    {
                        _TodoList.Enqueue(todo);
                    }
                }, null, ms, Timeout.Infinite);
            return todo.ID;
        }

        /// <summary>
        /// Cancels the task if it hasn't been performed already.
        /// </summary>
        /// <param name="cookie">Cookie that was returned as part of AddTask().</param>
        public void CancelTask(object cookie)
        {
            int id = (int)cookie;

            lock (_QueueLock) // modifying contents of queue so we need to lock it.
            {
                // Find the given task and mark it as cancelled.
                var pair = _TodoList.FirstOrDefault(qp => qp.ID == id);
                if (null != pair)
                {
                    pair.CancelTask = true;
                }
            }
        }
    }
}
